tsconfig.json의 include에 프로젝트 루트 경로의 파일을 추가하면 rootDir이 변경될 수 있음 {troubleshooting}
PR 머지 후 갑자기 발생한 도커 배포 문제
아래 에러 로그에 따르면 도커 컨테이너가 /app/dist/main을 찾을 수 없다는데..
racketime-api on  dev [$] via ⬢ v22.11.0 on 🐳 v27.5.1 took 3.7s
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/workspace/racketime-api/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 39.1s (13/13) FINISHED                                                                                                                             docker:desktop-linux
 => [app internal] load build definition from Dockerfile.prod                                                                                                                   0.0s
 => => transferring dockerfile: 652B                                                                                                                                            0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                                                  0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12)                                                                                                 0.0s
 => [app internal] load metadata for docker.io/library/node:lts                                                                                                                 0.9s
 => [app internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 112B                                                                                                                                               0.0s
 => [app internal] load build context                                                                                                                                           0.1s
 => => transferring context: 1.16MB                                                                                                                                             0.1s
 => [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                                   0.0s
 => => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                                               0.0s
 => CACHED [app builder 2/4] WORKDIR /app                                                                                                                                       0.0s
 => [app builder 3/4] COPY . /app                                                                                                                                               0.2s
 => [app builder 4/4] RUN npm i -g pnpm &&     pnpm install &&     npx prisma generate &&     pnpm run build                                                                   20.5s
 => CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl                                                                                                         0.0s
 => [app prod 4/5] COPY --chown=node:node --from=builder /app /app                                                                                                              2.2s
 => [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh                                                                                                                        0.3s
 => [app] exporting to image                                                                                                                                                   12.8s
 => => exporting layers                                                                                                                                                         9.8s
 => => exporting manifest sha256:e865d50bb6d0e58795de4cc2f259943fdee1c8c7c844f10a31a65f9c5bacc30b                                                                               0.0s
 => => exporting config sha256:cc2bb129c7424ccd0885f910fc1db13907d34293e98ec2aa175c6952ad30d29e                                                                                 0.0s
 => => exporting attestation manifest sha256:bd6dc726a0cfa611619938f22e2666b9c36cfbac25679306daaa94d838c30b61                                                                   0.0s
 => => exporting manifest list sha256:82cefa417aa2828567d7e9f563b055c7e9d9f001cee598649adbf32e23263d8d                                                                          0.0s
 => => naming to docker.io/library/infra-app:latest                                                                                                                             0.0s
 => => unpacking to docker.io/library/infra-app:latest                                                                                                                          3.0s
 => [app] resolving provenance for metadata file                                                                                                                                0.0s
[+] Running 2/2
 ✔ app                    Built                                                                                                                                                 0.0s
 ✔ Container infra-app-1  Recreated                                                                                                                                             1.0s
Attaching to app-1
app-1  | node:internal/modules/cjs/loader:1228
app-1  |   throw err;
app-1  |   ^
app-1  |
app-1  | Error: Cannot find module '/app/dist/main'
app-1  |     at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
app-1  |     at Function._load (node:internal/modules/cjs/loader:1055:27)
app-1  |     at TracingChannel.traceSync (node:diagnostics_channel:322:14)
app-1  |     at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
app-1  |     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
app-1  |     at node:internal/main/run_main_module:36:49 {
app-1  |   code: 'MODULE_NOT_FOUND',
app-1  |   requireStack: []
app-1  | }
app-1  |
app-1  | Node.js v22.14.0
app-1 exited with code 1
main 브랜치에서 똑같은 걸 하면..
잘 된다. 이건 머지하면서 뭐가 잘못 꼬인거다.
~/Downloads/app-b8fc19-250225_015236436 via ⬢ v22.11.0 on 🐳 v27.5.1
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/Downloads/app-b8fc19-250225_015236436/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 40.2s (14/14) FINISHED                                                                                                       docker:desktop-linux
 => [app internal] load build definition from Dockerfile.prod                                                                                             0.0s
 => => transferring dockerfile: 698B                                                                                                                      0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                            0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12)                                                                           0.0s
 => [app internal] load metadata for docker.io/library/node:lts                                                                                           1.8s
 => [app auth] library/node:pull token for registry-1.docker.io                                                                                           0.0s
 => [app internal] load .dockerignore                                                                                                                     0.0s
 => => transferring context: 158B                                                                                                                         0.0s
 => [app internal] load build context                                                                                                                     0.1s
 => => transferring context: 239.31kB                                                                                                                     0.0s
 => [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                             0.0s
 => => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                         0.0s
 => CACHED [app builder 2/4] WORKDIR /app                                                                                                                 0.0s
 => [app builder 3/4] COPY . /app                                                                                                                         0.1s
 => [app builder 4/4] RUN npm i -g pnpm &&     pnpm install &&     npx prisma generate &&     pnpm run build                                             22.6s
 => CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl                                                                                   0.0s
 => [app prod 4/5] COPY --chown=node:node --from=builder /app /app                                                                                        1.7s
 => [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh                                                                                                  0.3s
 => [app] exporting to image                                                                                                                             11.8s
 => => exporting layers                                                                                                                                   9.0s
 => => exporting manifest sha256:33099e4489f7619905bd1e24cb3549dc57718342128016c9b4bfdf6bccfe6a6e                                                         0.0s
 => => exporting config sha256:bebde6a9a02c95499840535f932d05fb0240be070bc33a27866062232662af3e                                                           0.0s
 => => exporting attestation manifest sha256:4f8247d773818dc9ab6ce5364dec9d6c2ea080ebf53d6f6640ec62baa12fb95f                                             0.0s
 => => exporting manifest list sha256:dea7e14826f30de52ceff975b5b3fcf95608913036016733d4dfcc0036210eda                                                    0.0s
 => => naming to docker.io/library/infra-app:latest                                                                                                       0.0s
 => => unpacking to docker.io/library/infra-app:latest                                                                                                    2.9s
 => [app] resolving provenance for metadata file                                                                                                          0.0s
[+] Running 2/2
 ✔ app                    Built                                                                                                                           0.0s
 ✔ Container infra-app-1  Recreated                                                                                                                       1.5s
Attaching to app-1
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [NestFactory] Starting Nest application...
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] VerificationModule dependencies initialized +16ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ProductLogModule dependencies initialized +0ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ConfigModule dependencies initialized +1ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
main, dev 빌드 이후 dist 디렉터리 내용물도 차이가 크다.
dev
얘는 src 안에 main.js가 들어갔는데? 어찌 된 일이지?
racketime-api/dist
➜ l
total 1056
drwxr-xr-x@  7 choiwheatley  staff     224 Mar 13 16:36 ./
drwxr-xr-x@ 31 choiwheatley  staff     992 Mar 13 16:43 ../
-rw-r--r--@  1 choiwheatley  staff     108 Mar 13 16:36 jest.config.d.ts
-rw-r--r--@  1 choiwheatley  staff     676 Mar 13 16:36 jest.config.js
-rw-r--r--@  1 choiwheatley  staff     515 Mar 13 16:36 jest.config.js.map
drwxr-xr-x@ 45 choiwheatley  staff    1440 Mar 13 16:36 src/
-rw-r--r--@  1 choiwheatley  staff  527286 Mar 13 16:36 tsconfig.build.tsbuildinfo
➜ ls src
academy               app.module.d.ts       coach-file            gamedatalog           metrix                reservation-code      tag                   verification
academy-bot-scheduler app.module.js         constant.d.ts         guidance              order                 settlement            tennis-content
academy-coach         app.module.js.map     constant.js           health                payment               shared                ticket
academy-tag           auth                  constant.js.map       main.d.ts             product-log           sita                  trainer-ota
admin                 business-info         court                 main.js               product-order         staff                 types
advertisement         coach-code            fb                    main.js.map           reservation           switch-bot            user
main
Downloads/app-b8fc19-250225_015236436/dist
➜ l
total 1104
drwxr-xr-x@ 47 choiwheatley  staff    1504 Mar 13 16:38 ./
drwx------@ 26 choiwheatley  staff     832 Mar 13 16:40 ../
drwxr-xr-x@ 19 choiwheatley  staff     608 Mar 13 16:38 academy/
...
-rw-r--r--@  1 choiwheatley  staff    3008 Mar 13 16:38 main.js
...
drwxr-xr-x@ 15 choiwheatley  staff     480 Mar 13 16:38 user/
drwxr-xr-x@  9 choiwheatley  staff     288 Mar 13 16:38 verification/
dev는 여기에 main.js 파일이 빠졌던 것이고 실행을 못했던 것이다. 다른 파일들도 하나도 안 들어가 있는 걸 보니 높은 확률로 빌드 실패가 뜬 것 같다. 어째서 빌드가 실패했는데 다음 스텝으로 넘어간 거지?
package.json 변경이력
아래처럼 해놨다. 내가 로컬에서 pnpm run build 하고 pnpm run start:prod가 자꾸 안되길래 저렇게 바꿔놓은 채로 실행한 거다. 빌드 결과는 손도 안 댔는데 언제부터 빌드위치가 dist/src로 바뀐거지?
- "start:prod": "node dist/main",
+ "start:prod": "node dist/src/main.js",
(CONCLUSION) tsconfig.json 쪽에 include 문제였다.
- "include": ["src/**/*.ts", "src/uploads/template.xlsx"]
+ "include": ["src/**/*.ts", "src/uploads/template.xlsx", "jest.config.ts"]
이걸 한 뒤로 dist 폴더구조가 바뀐 것이고 "jest.config.ts" 아이템을 제거하자 다시 dist 폴더 안에 컴파일 결과가 들어왔다. 원인이 뭐지?
🔍 원인 분석: include에 jest.config.ts 추가가 dist 구조를 바꾼 이유
결론:
tsconfig.json에서 include에 "jest.config.ts"를 추가했을 때, **TypeScript의 루트 디렉터리 감지(rootDir 설정)**가 달라졌기 때문이야.
💡 원리: rootDir 결정 방식
TypeScript는 tsconfig.json에서 include된 파일들의 공통 상위 디렉터리를 rootDir로 설정하고, outDir로 변환할 때 이 rootDir을 기준으로 상대 경로를 유지해.
- 즉, rootDir이 다르면dist/안에 빌드된 파일들의 구조가 달라짐.
- include에 추가한 파일이 기존과 다른 위치에 있으면- rootDir이 예상과 다르게 설정될 수 있음.
⚠️ jest.config.ts 추가가 왜 문제였나?
- 
원래는 include가"src/**/*.ts"였기 때문에 TypeScript는src/를rootDir로 인식하고dist/안에main.js등 관련 파일을 넣었음.src/ -> dist/ ├── main.ts -> ├── main.js ├── user/ -> ├── user/ ├── academy/ -> ├── academy/
- 
하지만 jest.config.ts는src/가 아닌 **루트 경로(/)**에 있었을 가능성이 높음.📁 프로젝트 루트 ├── src/ │ ├── main.ts │ ├── user/ │ ├── academy/ ├── jest.config.ts # ✅ include에 추가됨!
- 
TypeScript가 include된 모든 파일의 공통 상위 경로를rootDir로 설정하는데,- src/**/*.ts만 포함되었을 때 →- rootDir은- src/
- jest.config.ts를 포함했을 때 →- rootDir은 프로젝트 루트(- /)
 
- 
rootDir이/로 확장되면서 TypeScript는 다음처럼 파일 구조를 유지하려고 함:📁 dist/ ├── src/ ✅ 원래 dist 안에 없던 `src/` 폴더가 추가됨 │ ├── main.js │ ├── user/ │ ├── academy/ ├── jest.config.js→ 이 때문에 dist/main.js가 없어지고dist/src/main.js가 생긴 것!
🛠 해결 방법
✅ 해결책 1: jest.config.ts를 include에서 제거 (지금 해결한 방법)
✅ 해결책 2: tsconfig.json에 rootDir을 명시적으로 설정
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}
- 이렇게 하면 include가 변경되더라도 TypeScript가src/를rootDir로 고정하므로,dist/안에src/폴더가 생기지 않음.
✅ 해결책 3: exclude에 jest.config.ts 추가
{
  "exclude": ["jest.config.ts"]
}
- include에서- jest.config.ts를 제거하는 것과 동일한 효과.
📌 결론
✅ tsconfig.json의 include에 프로젝트 루트 경로의 파일을 추가하면 TypeScript의 rootDir이 변경될 수 있음.
✅ rootDir이 변경되면 빌드 결과의 dist/ 폴더 구조가 예상과 달라짐.
✅ 해결 방법: rootDir을 명시적으로 src/로 설정하거나 jest.config.ts를 include에서 제거. 🚀